<?php
/**
 * 
 * @todo description of this class
 * 
 * @author Alton Crossley <crossleyframework@nogahidebootstrap.com>
 * @package Crossley Framework
 *  
 * @copyright Copyright (c) 2003-2009, Nogaide BootStrap INC. All rights reserved.
 * @license BSD http://opensource.org/licenses/bsd-license.php
 * @version $Id:$
 * 
 */
/**
 * Abstract class for use in handeling data in the Crossley ORM
 * NOTE: this function simular to what was known as X_Abstract_DB_Table_Object
 * accept that it only uses a DB Adapter and is not RDMS dependent
 *
 * @author  Alton Crossley
 * @package Crossley Framework
 * @version 0.1
 */
abstract class X_Data_Abstract implements X_Interface_Titled
{
    const AUTO_QUERY = 'autoQuery';
    /**
     * set to true to save a trace of queries made with this object
     * NOTE: Large memory consumption - only for development usage
     *
     * @var bool
     */
    public static $bSaveHistory = false;
    
    /**
     * a history of queries
     *
     * @var array
     */
    public static $aQueryHistory = array();
    
    /**
     * table name(s) to be added to the query if any
     *
     * @var mutable
     */
    protected $_mTableName = null;
    
    /**
     * Name of the primary key. This must be the same name as the corresponding
     * field in the bind types array.
     *
     * @var string
     */
    protected $_sPrimaryKey = 'id';
    
    /**
     * the name of the file to pull the query from
     *
     * @var srting
     */
    public $sQueryFile;
    
    /**
     * name of the default select query in the QueryFile
     *
     * @var srting
     */
    public $sSelectName;
    
    /**
     * name of the insert query in the QueryFile
     *
     * @var string
     */
    public $sInsertName;
    
    /**
     * name of the update query in the QueryFile
     *
     * @var string
     */
    public $sUpdateName;
    
    /**
     * name of the delete query in the QueryFile
     *
     * @var string
     */
    public $sDeleteName;
    
    /**
     * name of the join query in the QueryFile
     *
     * @var string
     */
    public $sJoinName;
    
    /**
     * set to true to use join statement specified by $this->sJoinName
     *
     * @var bool
     */
    public $bUseJoin;
    
    /**
     * foreign key definition
     * in the following structure
     *
     * Array
     * (
     *     [<table_abbr>] => Array
     *     (
     *         [table] => <target table name>
     *         [table_abbr] => <target table abbr>
     *         [column] => <target column name>
     *         [from_table] => <FK source table name>
     *         [from_column] => <source column name>
     *         [class] => <class name to instanciate>
     *     )
     * )
     *
     *
     * @var array
     */
    protected $aFK = array();
    
    /**
     * set to true to instanciate children defined in $this->aFK on load()
     *
     * @var bool
     */
    public $bLoadChildren = false;
    
    /**
     * current query mode
     *
     * @var string
     */
    protected $_cMode = null;
    
    /**
     * refrence to the Data Adapter
     *
     * @var X_DB_MySQL_Adapter
     */
    protected $_oAdapter;
    
    /**
     * Bind types. This is the bind type array that matches up with the
     * X_DB_MySQL bind types
     * NOTE: when using FK functionality all attributes belonging in this
     * class MUST be defined here.  Any that are not will be passed on to the
     * children and removed from $this object.
     *
     * @var array
     */
    protected $_aBindTypes;
    
    /**
     * Array to map. This is used for creating an UPDATE statement if previously
     * a result data set from a select query wasn't run. For every DB column
     * that has been aliased you need to specify the actual column name in the
     * array below. Key is the alias, Value is the actual column name. The Alias
     * name is the name used in $this->_aBindTypes and $this->_aNoUpdate
     *
     * Array (<alias> => <column_name>)
     *
     * @var array
     */
    protected $_aAliasColumnMap;
    /**
     * Array of field names (these must be the same as the bind names) that
     * will not be automatically saved on an UPDATE if changed.
     *
     * @var array
     */
    protected $_aNoUpdate;
    
    /**
     * Tokens to bind or crossley::ANY
     *
     * @var array
     */
    public $aTokens;
    
    function __construct ($bUseJoin = false, $bLoadChildren = true)
    {
        $this->bUseJoin = $bUseJoin;
        $this->bLoadChildren = $bLoadChildren;
        $this->_setQuery();
    }
    
    /**
     * sets the query name based on bLoadFullDefinition
     * and bIndependentQuery properties
     *
     */
    protected function _setQuery ()
    {
        $this->sSelectName = ($this->bUseJoin) ? $this->sJoinSelectName : $this->sBaseSelectName;
    }
    
    /**
     * return the id of the current object
     *
     * @see X_Interface_Identifiable
     * @return int
     */
    public function getId ()
    {
        return $this->{$this->_sPrimaryKey};
    }
    
    /**
     * Get title from (1) provided property,
     * (2) title, (3) name, (4) type or nothing
     *
     * @see X_Interface_Titled
     * @return string
     */
    public function getTitle ()
    {
        // using func_get_args to not disturb Titled interface
        $sTitleAttr = (func_num_args()) ? func_get_arg(0) : 'title';
        
        switch (true)
        {
            case (isset($this->{$sTitleAttr})):
                return $this->{$sTitleAttr};
                break;
            
            case (isset($this->name)):
                return $this->name;
                break;
            
            case (isset($this->type)):
                return $this->type;
                break;
            
            case (isset($this->description)):
                return $this->description;
                break;
            
            default:
                return implode('|', $this->_aAttributes);
                break;
        }
    }
    
    /**
     * handle the adapter to save the information in the current object
     *
     * @param X_Interface_DB_Adapter $oAdapter
     * @return int the number of affected rows
     */
    public function save (X_Interface_DB_Adapter $oAdapter = null)
    {
    	if (is_null($oAdapter))
    	{
    		$this->_oAdapter = $oAdapter;
    	}
    	
    	if (!$this->_oAdapter instanceof X_Interface_DB_Adapter)
    	{
    		throw new X_Data_Exception("No adapter set for save");
    	}
    	
        // Determine if we need to update or insert
        if (! $this->isNew())
        {
            $sSQLName = $this->sUpdateName;
            $this->_cMode = X_SQL::UPDATE;
        }
        else
        {
            $sSQLName = $this->sInsertName;
            $this->_cMode = X_SQL::INSERT;
        }
        
        try
        {
            if ($sSQLName == self::AUTO_QUERY)
            {
                X_Debug::out(null, 'using AUTO query');
                return $this->_autoQuery();
            }
            else
            {
                X_Debug::out(null, 'using CONFIG query');
                return $this->_useConfigQuery($sSQLName);
            }
        }
        catch (Exception $oException)
        {
            $sQuery = $this->_oDb->getQuery();
            throw new X_DB_Exception($oException->getMessage() . "<pre>{$sQuery['query_with_binX_replaced']}</pre>");
        }
    }
    
    /**
     * this checks to see if the object came from storage or is new
     *
     * @return bool
     */
    public function isNew ()
    {
        return ($this->getId()) ? false : true;
    }
    /**
     * Runs the delete query from the class. If you want to flag your
     * class as deleted than set a different delete query that runs an update.
     *
     * @todo   no auto query stuff yet
     * @param  X_Interface_DB_Adapter $oAdapter
     * @return int the number of affected rows
     */
    public function delete (X_Interface_DB_Adapter $oAdapter)
    {
        if ($this->isNew()) return;
        $this->_oAdapter = $oAdapter;
        return $this->_useConfigQuery($this->sDeleteName);
    }
    /**
     * Automatically perform the needed query
     *
     * @todo   look at this... probably doesn't work
     * @return int affected rows
     */
    protected function _autoQuery ()
    {
        // TODO: X_SQL should be renamed X_SQL_Builder and should take advantage
        // of a database adapter
        // NOTE:  this is a hack.
        // TODO: handle errors in a nice way
        $rConn = $this->_oAdapter->getDBCON();
        $oSQL_Builder = new X_SQL($this->_cMode, $this->_oAdapter->sDBName, $this->_mTableName, $rConn);
        
        //X_Debug::out($this->_aAttributes, '$this->_aAttributes '.__CLASS__.' Line: '.__LINE__);
        


        $oSQL_Builder->sWhereColumn = $this->_sPrimaryKey;
        $oSQL_Builder->aRow = &$this->_aAttributes;
        $sSQL = $oSQL_Builder->buildSQL();
        
        //X_Debug::out($sSQL, '$sSQL '.__CLASS__.' Line: '.__LINE__);
        


        switch ($this->_cMode)
        {
            /* Luke commented this out -
			   SELECT doesn't make sense in here since you don't have
			   an instantiated instance mostly. The adapter is the SELECT query
			   workhorse. Methods collect() and fill()

			  case X_SQL::SELECT :
		      $oResult = $rConn->query($sSQL);
				while ($aRow = $oResult->fetch_assoc())
				{
					$this->_aAttributes[] = $aRow;
				}
				if (count($this->_aAttributes) == 1)
				{
					$this->_aAttributes = $this->_aAttributes[0];
					return 1;
				}
				else
				{
					return count($this->_aAttributes);
				}
				break;*/
            case X_SQL::DELETE:
            case X_SQL::INSERT:
            case X_SQL::UPDATE:
            default:
                $rConn->query($sSQL);
                $iRows = $rConn->affected_rows;
                if ($this->_cMode == X_SQL::INSERT) // set id for INSERT
                {
                    $this->{$this->_sPrimaryKey} = $rConn->insert_id;
                }
                return $iRows;
                break;
        }
    }
    /**
     * execute the configured query
     *
     * @param string $sSQLName
     * @return int|bool affected rows or false on failure
     */
    protected function _useConfigQuery ($sSQLName)
    {
        // init bind array
        $aBind = array();
        // set the query
        $this->_oAdapter->reset();
        
        $this->_oAdapter->sQueryFile = $this->sQueryFile;
        $this->_oAdapter->sQuery = $sSQLName;
        $this->_oAdapter->sTableName = $this->_mTableName;
        
        // Get the bind tokens
        $this->aTokens = array_unique($this->_oAdapter->getBindTokens());
        
        // If in UPDATE mode we need to get the changed values and add SETs
        if ($this->_cMode == X_SQL::UPDATE)
        {
            if (! $this->_addUpdateFields())
            {
                X_Debug::out(null, 'UPDATE Failed');
                return false; // No fields were changed
            }
        }
        
        // Fill the bind array
        $this->_fillBindArray($aBind);
        
        // Actually bind the data from the bind array
        $this->_oAdapter->bindData($aBind, $this->_aBindTypes);
        
        // Run and return get affected rows since this won't handle SELECT queries
        $iAffectedRows = $this->_oAdapter->getAffectedRows();
        
        if ($this->_cMode == X_SQL::INSERT) // set id for INSERT
        {
            $this->{$this->_sPrimaryKey} = $this->_oAdapter->getInsertId();
        }
        
        if (self::$bSaveHistory)
        {
            $aQueryies = $this->_oAdapter->getQuery();
            self::$aQueryHistory[] = $aQueryies['query_with_binX_replaced'];
        }
        
        return $iAffectedRows;
    }
    /**
     * Fills the bind arrays. Moved to a separate function because the magical
     * world of figuring out what fields go in the bind array is different
     * for INSERTS and the other query modes.
     *
     * @param unknown_type $aBind
     */
    protected function _fillBindArray (&$aBind)
    {
        // Fill bind array for INSERTs
        if ($this->_cMode == X_SQL::INSERT)
        {
            foreach ($this->_aAttributes as $sKey => &$mValue)
            {
                if (! in_array(":$sKey", $this->aTokens))
                {
                    //X_Debug::out($sKey." NOT found! ".__CLASS__." line: ".__LINE__);
                }
                else
                {
                    $aBind[$sKey] = $mValue;
                }
            }
        }
        // Fill bind array for UPDATE and DELETE
        else
        {
            foreach ($this->aTokens as $sKey)
            {
                $sKey = substr($sKey, 1); // remove colon
                if (! array_key_exists($sKey, $this->_aAttributes))
                {
                    //X_Debug::out($sKey." NOT found! ".__CLASS__." line: ".__LINE__);
                }
                else
                {
                    $aBind[$sKey] = $this->_aAttributes[$sKey];
                }
            }
        }
    }
    /**
     * Adds a dynamic SET for each changed field. Also adds the bind vars to
     * the tokens array.
     *
     * @return bool false if no fields are changed on the instance
     */
    protected function _addUpdateFields ()
    {
        $aChanged = $this->getChangedValues();
        // Return false if no fields were changed
        if (empty($aChanged))
        {
            return false;
        }
        
        $aMetaData = $this->_oAdapter->getMetaData();
        // Add to the tokens array if it hasn't already been added to
        $iSETClausesAdded = 0;
        foreach ($aChanged as $sFieldName => $mNewValue)
        {
            // Don't add a SET condition if the field is specified in "NO UPDATE" array
            if (in_array($sFieldName, $this->_aNoUpdate))
            {
                continue; // next loop
            }
            $iSETClausesAdded ++;
            $sFieldBindName = ":$sFieldName";
            // If the res meta data isn't available then use the alias column map
            if (empty($aMetaData))
            {
                if (array_key_exists($sFieldName, $this->_aAliasColumnMap))
                {
                    $sDBColName = $this->_aAliasColumnMap[$sFieldName];
                }
                else
                {
                    $sDBColName = $sFieldName; // Assume it wasn't aliased
                }
            }
            else if (! $sDBColName = @$this->_oAdapter->getMetaData($sFieldName)->orgname)
            {
                // otherwise use the meta data to retrieve the actual column name
                trigger_error("Orig column for $sFieldName not found in meta data", E_USER_ERROR);
            }
            
            $this->_oAdapter->addSet('`' . $sDBColName . '`', $sFieldBindName);
            
            // Add to the tokens
            $this->aTokens[] = $sFieldBindName;
        }
        // Don't run the UPDATE query if all the changed fields were in the noUpdate array.
        if ($iSETClausesAdded == 0)
        {
            return false;
        }
        $this->aTokens = array_unique($this->aTokens);
        return true;
    }
    
    /**
     * Accessor for the table's primary key
     *
     * @return string
     */
    public function getPrimaryKey ()
    {
        return $this->_sPrimaryKey;
    }
    
    /**
     * Accessor for the table name
     *
     * @return mutable
     */
    public function getTableName ()
    {
        return $this->_mTableName;
    }
    
    /**
     * Accessor for bind types
     *
     * @return array
     */
    public function getBindTypes ()
    {
        return (array) $this->_aBindTypes;
    }
    
    /**
     * Called from adapter->fill(). A table object must define how it fills
     * itself. It gives all rows from the query set on the adapter and you must
     * define how to fill it.
     *
     * @param array $aRows all row results of the SELECT query
     */
    public function consume (array $aRow)
    {
        parent::setAttributes($aRow);
    }
    
    /**
     * Load this instance by primary key
     *
     * @param mixed $mId integer | string
     * @param X_DB_MySQL_Adapter $oAdapter
     */
    public function load (X_DB_MySQL_Adapter $oAdapter, $xID = null)
    {
        $this->_validate();
        
        $oAdapter->addWhere($this->_sPrimaryKey . ' = :primary_key');
        
        if ($xID === null)
        {
            $xID = $this->getId();
            $oAdapter->bindByName('primary_key', $xID);
            if (empty($xID)) trigger_error('no value for primary key', E_USER_WARNING);
        }
        
        $oAdapter->bindByName('primary_key', $xID);
        $oAdapter->fill($this);
    }
    
    protected function _validate ()
    {
        if (empty($this->sQueryFile)) trigger_error('$sQueryFile not set on ' . __CLASS__);
        if (empty($this->sSelectName)) trigger_error('$sSelectName not set on ' . __CLASS__);
        if (empty($this->_sPrimaryKey)) trigger_error('$_sPrimaryKey not set on ' . __CLASS__);
    }
    
    /**
     * make this a new record in the db, by deleting the primary key
     *
     */
    public function makeNew ()
    {
        unset($this->_aAttributes[$this->_sPrimaryKey]);
        return $this;
    }
    
    /**
     * handle cloning
     *
     */
    public function __clone ()
    {
        $this->makeNew();
    }
    
    public static function build (array $aRows)
    {
        $this->setAttributes($aRows);
    }
}